﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Runtime.Serialization;
using System.Xml.Serialization;

namespace Ns
{
    /// <summary>
    /// Represents a value type associated to an <see cref="Unit"/>. 
    /// Its gives support for scale and arithmetic operations by considering operand units.
    /// </summary>
    /// <typeparam name="T">The underlying type.</typeparam>
#if !SILVERLIGHT
    [Serializable]
#endif
    [DataContract(Name = "MeasureOf{0}")]
    public struct Measure<T>
        : IFormattable
        , IComparable<Measure<T>>
        , IEquatable<Measure<T>>            
    {
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private T _value;

        /// <summary>
        /// Gets the value of the measure.
        /// </summary>
        /// <value>The measure value.</value>
        /// <remarks>The set operation is not private to let the <see cref="XmlSerializer"/> 
        /// accessing the property during serializing/deserializing operations.</remarks>
        [DataMember(EmitDefaultValue = false, Order = 1)]
        [XmlElement(ElementName = "Value", Order = 1)]
        public T Value 
        {
            get { return _value; }
            set { _value = value; } 
        }

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private Unit _unit;

        /// <summary>
        /// Gets the unit of the measure.
        /// </summary>
        /// <value>The measure unit.</value>
        [XmlIgnore]
        public Unit Unit
        {
            get { return _unit; }
            private set { _unit = value; }
        }

        /// <summary>
        /// Gets or sets the serialized unit of the measure.
        /// </summary>
        /// <value>The serialized measure unit.</value>
        [DataMember(EmitDefaultValue = false, Name = "Unit", Order = 2)]
        [XmlElement(ElementName = "Unit", Order = 2)]
        public string SerializedUnit
        {
            get { return Unit; }
            set { Unit = (Unit)value; }
        }

        private IEnumerable<Unit> Units
        {
            get
            {
                return (Unit != null)
                    ? Unit.Units
                    : null;
            }
        }

        private IEnumerable<Unit> InnerUnits
        {
            get
            {
                return (Unit != null)
                    ? Unit.InnerUnits
                    : null;
            }
        }        

        /// <summary>
        /// Initializes a new instance of the <see cref="Measure&lt;T&gt;"/> struct.
        /// </summary>
        /// <param name="value">The measure value.</param>
        public Measure(T value)
            : this(value, null)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Measure&lt;T&gt;"/> struct.
        /// </summary>
        /// <param name="value">The measure value.</param>
        /// <param name="unit">The measure unit.</param>
        public Measure(T value, Unit unit)
        {
            _value = value;
            _unit = unit;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Measure&lt;T&gt;"/> struct.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <param name="unit">The unit.</param>
        public Measure(T value, string unit)
        {
            _value = value;
            _unit = new Unit(unit);
        }

        #region IFormattable Members
        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <param name="formatProvider">The format provider.</param>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        /// <exception cref="InvalidOperationException">The underlying type is not implementing the <see cref="IFormattable"/> interface.</exception>
        public string ToString(IFormatProvider formatProvider)
        {
            return ToString(null, formatProvider);
        }

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <param name="format">The format.</param>
        /// <param name="formatProvider">The format provider.</param>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        /// <exception cref="InvalidOperationException">The underlying type is not implementing the <see cref="IFormattable"/> interface.</exception>
        public string ToString(string format, IFormatProvider formatProvider)
        {
            var unit = Unit != null
                ? Unit.ToString()
                : string.Empty;

            if (!typeof(IFormattable).IsAssignableFrom(typeof(T)))
                throw new InvalidOperationException(string.Format("The type {0} does not implement the {1} interface."
                    , typeof(T).Name
                    , typeof(IFormattable).FullName));

            return unit != string.Empty
                ? string.Format("{0} {1}", ((IFormattable)Value).ToString(format, formatProvider), unit)
                : string.Format("{0}", ((IFormattable)Value).ToString(format, formatProvider));
        }

        #endregion

        #region Overriden Members

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString()
        {
            return Unit != null
                ? string.Format("{0} {1}", Value, Unit)
                : string.Format("{0}", Value);
        }

        /// <summary>
        /// Returns a hash code for this instance.
        /// </summary>
        /// <returns>
        /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. 
        /// </returns>
        public override int GetHashCode()
        {
            int hash = 0x4043ed47;
            hash = (hash * -1521134295) + Value.GetHashCode();
            hash = (hash * -1521134295) + (Unit == null ? 0 : Unit.GetHashCode());
            return hash;
        }

        /// <summary>
        /// Determines whether the specified <see cref="System.Object"/> is equal to this instance.
        /// </summary>
        /// <param name="obj">The <see cref="System.Object"/> to compare with this instance.</param>
        /// <returns>
        /// 	<b>true</b> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <b>false</b>.
        /// </returns>
        public override bool Equals(object obj)
        {
            return (obj is Measure<T>)
                ? Equals((Measure<T>)obj)
                : false;
        }

        #endregion

        #region IEquatable<Measure<T>> Members

        /// <summary>
        /// Indicates whether the current object is equal to another object of the same type.
        /// </summary>
        /// <param name="other">An object to compare with this object.</param>
        /// <returns>
        /// true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
        /// </returns>
        public bool Equals(Measure<T> other)
        {
            return object.Equals(Value, other.Value) && Unit.Equals(Unit, other.Unit);
        }

        #endregion

        #region IComparable<Measure<T>> Members

        /// <summary>
        /// Compares the current object with another object of the same type.
        /// </summary>
        /// <param name="other">An object to compare with this object.</param>
        /// <returns>
        /// A 32-bit signed integer that indicates the relative order of the objects being compared. The return value has the following meanings:
        /// Value
        /// Meaning
        /// Less than zero
        /// This object is less than the <paramref name="other"/> parameter.
        /// Zero
        /// This object is equal to <paramref name="other"/>.
        /// Greater than zero
        /// This object is greater than <paramref name="other"/>.
        /// </returns>
        public int CompareTo(Measure<T> other)
        {
            var thisScaled = default(Measure<T>);
            var otherScaled = default(Measure<T>);
            Scale(this, other, out thisScaled, out otherScaled);

            if (!Unit.Equals(thisScaled.Unit, otherScaled.Unit))
                throw new ArgumentException("Non comparable units.");

            return Comparer<T>.Default.Compare(Value, other.Value);
        }

        #endregion

        private T ChangeType(object value)
        {
            //consider generic type
            var simpleType = Nullable.GetUnderlyingType(typeof(T));
            return (T)Convert.ChangeType(value
                , simpleType == null
                ? typeof(T)
                : simpleType
                , CultureInfo.InvariantCulture);
        }

        /// <summary>
        /// Scales the current <see cref="Measure&lt;T&gt;"/> to a specified <see cref="UnitPrefix"/> by considering its root unit.
        /// </summary>
        /// <param name="prefix">The scaling prefix.</param>
        /// <returns>A <see cref="Measure&lt;T&gt;"/> scaled to the specified <paramref name="prefix"/>.</returns>
        /// <example>
        /// <code>
        /// 13 cm3/€ ScaleTo(UnitPrefixes.Deci) => 0,013 dm3/€ 
        /// </code>
        /// </example>
        /// <exception cref="InvalidOperationException">The current <see cref="Measure&lt;T&gt;"/> does not have an <see cref="Unit"/>.</exception>
        public Measure<T> ScaleTo(UnitPrefix prefix)
        {
            if (Unit == null)
                throw new InvalidOperationException("Scaling is not possible on measure with no unit.");

            if (prefix.CompareTo(Unit.Prefix) == 0)
                return this;
                        
            var value = Operator<T>.Multiply(Value
                , ChangeType(Math.Pow(10, (Unit.Prefix.Exponent - prefix.Exponent) * Unit.Exponent)));

            return new Measure<T>(value
                , new Unit(prefix
                    , Unit.Name
                    , Unit.Exponent
                    , Unit.InnerUnit));
        }

        /// <summary>
        /// Scales the specified inner <see cref="Unit"/> of the current <see cref="Measure&lt;T&gt;"/> to the specified <see cref="UnitPrefix"/>.
        /// </summary>
        /// <param name="innerUnit">The inner unit to scale.</param>
        /// <param name="prefix">The scaling prefix.</param>
        /// <returns>A <see cref="Measure&lt;T&gt;"/> with the inner <see cref="Unit"/> scaled to the specified <paramref name="prefix"/>.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="innerUnit"/> is null.</exception>
        /// <exception cref="ArgumentException"><paramref name="innerUnit"/> not found in the current <see cref="Measure&lt;T&gt;"/>.</exception>
        /// <example>
        /// <code>13 cm3/€ ScaleInnerUnitTo(€, UnitPrefixes.Kilo) => 13000 cm3/k€ </code>
        /// </example>
        public Measure<T> ScaleInnerUnitTo(Unit innerUnit, UnitPrefix prefix)
        {
            if (innerUnit == null)
                throw new ArgumentNullException("innerUnit");

            var units = Unit != null ? Units.ToList() : new List<Unit>();
            var index = units.FindIndex(match => Unit.Equals(match, innerUnit));
            if (index == -1)
                throw new ArgumentException("Inner unit not found.", "innerUnit");

            if (prefix.CompareTo(innerUnit.Prefix) == 0)
                return this;

            var value = Operator<T>.Multiply(Value
                , ChangeType(Math.Pow(10, (prefix.Exponent - innerUnit.Prefix.Exponent) * innerUnit.Exponent)));

            units[index] = new Unit(prefix, innerUnit.Name, innerUnit.Exponent);
            return new Measure<T>(value, new Unit(units));
        }

        #region Operators


        /// <summary>
        /// Performs an explicit conversion from <typeparamref name="T"/> to <see cref="Measure&lt;T&gt;"/>.
        /// </summary>                
        /// <param name="value">The value.</param>
        /// <returns>The result of the conversion.</returns>       
        public static explicit operator Measure<T>(T value)
        {
            return new Measure<T>(value);
        }

        /// <summary>
        /// Performs an implicit conversion from <see cref="Measure&lt;T&gt;"/> to <typeparamref name="T"/>.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <returns>The result of the conversion.</returns>
        public static implicit operator T(Measure<T> value)
        {
            return value.Value;
        }

        /// <summary>
        /// Implements the operator +.
        /// </summary>
        /// <param name="x">The x.</param>
        /// <param name="y">The y.</param>
        /// <returns>The result of the operator according to the lowest <see cref="Unit"/> when applicable.</returns>        
        /// <exception cref="ArgumentException"><paramref name="x"/> and <paramref name="y"/> have a different unit.</exception>
        /// <example><code>3 €/g + 2  €/kg = 3,002 €/g</code></example>
        public static Measure<T> operator +(Measure<T> x, Measure<T> y)
        {
            if (x.Unit == null)
                return (y.Unit == null)
                    ? new Measure<T>(Operator<T>.Add(x.Value, y.Value), null)
                    : new Measure<T>(Operator<T>.Add(x.Value, y.Value), y.Unit);
            if (y.Unit == null)
                return new Measure<T>(Operator<T>.Add(x.Value, y.Value), x.Unit);

            var xScaled = default(Measure<T>);
            var yScaled = default(Measure<T>);
            Scale(x, y, out xScaled, out yScaled);

            if (!Unit.Equals(xScaled.Unit, yScaled.Unit))
                throw new ArgumentException("Operation not possible on measures with different unit.");

            var value = Operator<T>.Add(xScaled.Value, yScaled.Value);
            return new Measure<T>(value, xScaled.Unit);
        }

        /// <summary>
        /// Implements the operator +.
        /// </summary>
        /// <param name="x">The x.</param>
        /// <param name="y">The y.</param>
        /// <returns>The result of the operator.</returns>        
        public static Measure<T> operator +(Measure<T> x, T y)
        {
            var value = Operator<T>.Add(x.Value, y);
            return new Measure<T>(value, x.Unit);
        }

        /// <summary>
        /// Implements the operator -.
        /// </summary>
        /// <param name="x">The x.</param>
        /// <param name="y">The y.</param>
        /// <returns>The result of the operator according to the lowest <see cref="Unit"/> when applicable.</returns>
        /// <exception cref="ArgumentException"><paramref name="x"/> and <paramref name="y"/> have a different unit.</exception>
        /// <example><code>3 €/g – 2 €/kg = 2,998 €/g</code></example>
        public static Measure<T> operator -(Measure<T> x, Measure<T> y)
        {
            if (x.Unit == null)
                return (y.Unit == null)
                    ? new Measure<T>(Operator<T>.Subtract(x.Value, y.Value), null)
                    : new Measure<T>(Operator<T>.Subtract(x.Value, y.Value), y.Unit);
            if (y.Unit == null)
                return new Measure<T>(Operator<T>.Subtract(x.Value, y.Value), x.Unit);

            var xScaled = default(Measure<T>);
            var yScaled = default(Measure<T>);
            Scale(x, y, out xScaled, out yScaled);

            if (!Unit.Equals(xScaled.Unit, yScaled.Unit))
                throw new ArgumentException("Operation not possible on measures with different unit.");

            var value = Operator<T>.Subtract(xScaled.Value, yScaled.Value);
            return new Measure<T>(value, xScaled.Unit);
        }

        /// <summary>
        /// Implements the operator -.
        /// </summary>
        /// <param name="x">The x.</param>
        /// <param name="y">The y.</param>
        /// <returns>The result of the operator.</returns>
        public static Measure<T> operator -(Measure<T> x, T y)
        {
            var value = Operator<T>.Subtract(x.Value, y);
            return new Measure<T>(value, x.Unit);
        }

        private static void Scale(Measure<T> x, Measure<T> y, out Measure<T> xScaled, out Measure<T> yScaled)
        {
            if (x.Unit == null || y.Unit == null)
            {
                xScaled = x;
                yScaled = y;
                return;
            }

            //scale according to unit root
            var xs = (Unit.Equals(x.Unit, y.Unit, UnitFields.Name)
                && y.Unit != null
                && y.Unit.Prefix.CompareTo(x.Unit.Prefix) < 0)
                ? x.ScaleTo(y.Unit.Prefix)
                : x;

            var ys = (Unit.Equals(x.Unit, y.Unit, UnitFields.Name)
                && x.Unit != null
                && x.Unit.Prefix.CompareTo(y.Unit.Prefix) < 0)
                ? y.ScaleTo(x.Unit.Prefix)
                : y;

            //scale according to inner unit
            var tuples = from xiu in xs.InnerUnits
                         join yiu in ys.InnerUnits
                         on (xiu.Name ?? string.Empty).ToString(CultureInfo.InvariantCulture)
                         equals (yiu.Name ?? string.Empty).ToString(CultureInfo.InvariantCulture)
                         select new { X = xiu, Y = yiu };

            foreach (var tuple in tuples)
            {
                xs = tuple.Y.Prefix.CompareTo(tuple.X.Prefix) < 0
                    ? xs.ScaleInnerUnitTo(tuple.X, tuple.Y.Prefix)
                    : xs;

                ys = tuple.X.Prefix.CompareTo(tuple.Y.Prefix) < 0
                    ? ys.ScaleInnerUnitTo(tuple.Y, tuple.X.Prefix)
                    : ys;
            }

            //scale according to unit root & inner unit
            var xInnerUnit = (from iu in xs.InnerUnits
                              where string.Equals(iu.Name, ys.Unit.Name, StringComparison.InvariantCulture)
                              select iu).FirstOrDefault(); //unit duplicates are not authorized

            if (xInnerUnit != null)
            {
                xs = xInnerUnit.Prefix.CompareTo(ys.Unit.Prefix) > 0
                    ? xs.ScaleInnerUnitTo(xInnerUnit, ys.Unit.Prefix)
                    : xs;

                ys = xInnerUnit.Prefix.CompareTo(ys.Unit.Prefix) < 0
                    ? ys.ScaleTo(xInnerUnit.Prefix)
                    : ys;
            }

            var yInnerUnit = (from iu in ys.InnerUnits
                              where string.Equals(iu.Name, xs.Unit.Name, StringComparison.InvariantCulture)
                              select iu).FirstOrDefault(); //unit duplicates are not authorized

            if (yInnerUnit != null)
            {
                xs = yInnerUnit.Prefix.CompareTo(xs.Unit.Prefix) < 0
                    ? xs.ScaleTo(yInnerUnit.Prefix)
                    : xs;

                ys = yInnerUnit.Prefix.CompareTo(xs.Unit.Prefix) > 0
                    ? ys.ScaleInnerUnitTo(yInnerUnit, xs.Unit.Prefix)
                    : ys;
            }

            xScaled = xs;
            yScaled = ys;
        }

        /// <summary>
        /// Implements the operator *.
        /// </summary>
        /// <param name="x">The x.</param>
        /// <param name="y">The y.</param>
        /// <returns>The result of the operator according to the lowest <see cref="Unit"/> when applicable.</returns>
        /// <exception cref="ArgumentException">Operation result represents a measure with a composite unit.</exception>
        /// <example><code>100 €/m * 3 km = 300000 €</code></example>
        public static Measure<T> operator *(Measure<T> x, Measure<T> y)
        {
            if (x.Unit == null)
                return (y.Unit == null)
                    ? new Measure<T>(Operator<T>.Multiply(x.Value, y.Value), null)
                    : new Measure<T>(Operator<T>.Multiply(x.Value, y.Value), y.Unit);
            if (y.Unit == null)
                return new Measure<T>(Operator<T>.Multiply(x.Value, y.Value), x.Unit);

            var xScaled = default(Measure<T>);
            var yScaled = default(Measure<T>);
            Scale(x, y, out xScaled, out yScaled);

            var unit = Unit.Multiply(xScaled.Unit, yScaled.Unit);
            var value = Operator<T>.Multiply(xScaled.Value, yScaled.Value);

            return new Measure<T>(value, unit);
        }

        /// <summary>
        /// Implements the operator *.
        /// </summary>
        /// <param name="x">The x.</param>
        /// <param name="y">The y.</param>
        /// <returns>The result of the operator.</returns>
        public static Measure<T> operator *(Measure<T> x, T y)
        {
            var value = Operator<T>.Multiply(x.Value, y);
            return new Measure<T>(value, x.Unit);
        }

        /// <summary>
        /// Implements the operator /.
        /// </summary>
        /// <param name="x">The x.</param>
        /// <param name="y">The y.</param>
        /// <returns>The result of the operator according to the lowest <see cref="Unit"/> when applicable.</returns>
        /// <exception cref="ArgumentException">Operation result represents a measure with a composite unit.</exception>
        /// <example><code>100 €/km2 / 3 m = 0.00003 €/m3</code></example>
        public static Measure<T> operator /(Measure<T> x, Measure<T> y)
        {
            if (x.Unit == null)
                return (y.Unit == null)
                    ? new Measure<T>(Operator<T>.Add(x.Value, y.Value), null)
                    : new Measure<T>(Operator<T>.Add(x.Value, y.Value), y.Unit);
            if (y.Unit == null)
                return new Measure<T>(Operator<T>.Add(x.Value, y.Value), x.Unit);

            var xScaled = default(Measure<T>);
            var yScaled = default(Measure<T>);
            Scale(x, y, out xScaled, out yScaled);

            var unit = Unit.Divide(xScaled.Unit, yScaled.Unit);
            var value = Operator<T>.Divide(xScaled.Value, yScaled.Value);

            return new Measure<T>(value, unit);
        }

        /// <summary>
        /// Implements the operator /.
        /// </summary>
        /// <param name="x">The x.</param>
        /// <param name="y">The y.</param>
        /// <returns>The result of the operator.</returns>
        public static Measure<T> operator /(Measure<T> x, T y)
        {
            var value = Operator<T>.Divide(x.Value, y);
            return new Measure<T>(value, x.Unit);
        }

        /// <summary>
        /// Implements the operator ==.
        /// </summary>
        /// <param name="left">The left <see cref="Measure&lt;T&gt;"/>.</param>
        /// <param name="right">The right <see cref="Measure&lt;T&gt;"/>.</param>
        /// <returns>The result of the operator.</returns>
        public static bool operator ==(Measure<T> left, Measure<T> right)
        {
            return object.ReferenceEquals(left, right) || (!object.Equals(left, null) && !object.Equals(right, null) && left.Equals(right));
        }

        /// <summary>
        /// Implements the operator ==.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static bool operator ==(object left, Measure<T> right)
        {
            return left is Measure<T>
                ? ((Measure<T>)left) == right
                : false;
        }

        /// <summary>
        /// Implements the operator ==.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static bool operator ==(Measure<T> left, object right)
        {
            return right is Measure<T>
                ? left == ((Measure<T>)right)
                : false;
        }

        /// <summary>
        /// Implements the operator !=.
        /// </summary>
        /// <param name="left">The left <see cref="Measure&lt;T&gt;"/>.</param>
        /// <param name="right">The right <see cref="Measure&lt;T&gt;"/>.</param>
        /// <returns>The result of the operator.</returns>
        public static bool operator !=(Measure<T> left, Measure<T> right)
        {
            return !(left == right);
        }

        /// <summary>
        /// Implements the operator !=.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static bool operator !=(object left, Measure<T> right)
        {
            return left is Measure<T>
                ? ((Measure<T>)left) != right
                : false;
        }

        /// <summary>
        /// Implements the operator ==.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static bool operator !=(Measure<T> left, object right)
        {
            return right is Measure<T>
                ? left != ((Measure<T>)right)
                : false;
        }

        #endregion        
    }
}